using System;
using System.IO;

namespace PTP.Core
{
  /// <summary>
  /// Container stream is for reading byte stream that contains PTP containers.
  /// Read-method returns only payload bytes and only after ReadContainer-method is called.
  /// Header data is only available trough ReadContainer-method. 
  /// Same logic applies to writing. No data can be sent unless WriteContainer or WriteHeader is called.
  /// </summary>
  public class ContainerStream : Stream
  {
    protected Stream innerStream;

    protected object commandLock = new object();

    int nextReadHeader = 0;
    int positionInCurrentReadContainer = 0;

    int nextWriteHeader = 0;
    int positionInCurrentWriteContainer = 0;

    public ContainerStream(Stream innerStream)
    {
      this.innerStream = innerStream;
    }

    public override bool CanRead
    {
      get { return this.innerStream.CanRead; }
    }

    public override bool CanSeek
    {
      get { return this.innerStream.CanSeek; }
    }

    public override bool CanWrite
    {
      get { return this.innerStream.CanWrite; }
    }

    public override void Flush()
    {
      this.innerStream.Flush();
    }

    public override long Length
    {
      get { return this.nextReadHeader + this.positionInCurrentReadContainer; }
    }

    public override long Position
    {
      get
      {
        return this.positionInCurrentReadContainer;
      }
      set
      {
        throw new NotSupportedException();
      }
    }

    #region Reading

    public override int Read(byte[] buffer, int offset, int count)
    {
      int amountToRead = count;
      if (amountToRead > this.nextReadHeader)
        amountToRead = this.nextReadHeader;

      if (this.nextReadHeader <= 0)
        return 0;

      int result = this.innerStream.Read(buffer, offset, count);

      this.nextReadHeader -= result;
      this.positionInCurrentReadContainer += result;

      return result;
    }

    public Container ReadContainer()
    {
      return ReadContainer(-1, false);
    }

    public virtual Container ReadContainer(int expectedTransactionID, bool synchronized)
    {
      while (this.nextReadHeader > 0)
      {
        int nextStop = this.nextReadHeader + this.positionInCurrentReadContainer;
        if (this.Seek(this.nextReadHeader, SeekOrigin.Current) < nextStop)
          throw new Exception("No more data");
      }

      Container result = getContainer(synchronized);
      if ((expectedTransactionID >= 0) && (expectedTransactionID != result.Header.TransactionID))
        throw new Exception("TransactionID doesn't match. Expected: " + expectedTransactionID + ", Result: " + result.Header.TransactionID);

#if DEBUG
      if (DebugUtil.TraceEnabled)
      {
        DebugUtil.Write("ContainerStream", "Header.Length: " + result.Header.Length);
        DebugUtil.Write("ContainerStream", "ContainerType: " + result.Header.ContainerType);
        DebugUtil.Write("ContainerStream", "Code: " + DebugUtil.ToHexDigits((uint)result.Header.Code));
        DebugUtil.Write("ContainerStream", "TransactionID: " + result.Header.TransactionID);
        DebugUtil.Write("ContainerStream", String.Empty);
      }
#endif

      return result;
    }

    protected Container getContainer(bool synchronized)
    {
      ContainerHeader header = new ContainerHeader(this.innerStream);
      this.nextReadHeader = header.PayloadLength;
      this.positionInCurrentReadContainer = 0;

      switch (header.ContainerType)
      {
        case ContainerType.DataBlock:
          return new DataBlockContainer(header, this);
        case ContainerType.ResponseBlock:
          if (synchronized)
            System.Threading.Monitor.Exit(commandLock);
          return new ResponseBlockContainer(header, this); //Give current instance as stream, because we need keep track on the distance to next header
        case ContainerType.CommandBlock:
        case ContainerType.EventBlock:
          throw new Exception("Invalid container type. " + header.ContainerType);
      }
      throw new Exception("Unkown container type");
    }

    public override long Seek(long offset, SeekOrigin origin)
    {
      if (offset < 0)
        throw new ArgumentException("offset must be positive number");
      if (origin != SeekOrigin.Current)
        throw new ArgumentException("only SeekOrigin.Current is supported");

      int amountToRead = (int)offset;
      if (amountToRead > this.nextReadHeader)
        amountToRead = this.nextReadHeader;

      if (this.nextReadHeader <= 0)
        return this.positionInCurrentReadContainer;

      this.innerStream.Seek(amountToRead, origin); // Don't check the result, because stream implementation doesn't keep track of it's position

      this.nextReadHeader -= amountToRead;
      if (this.nextReadHeader < 0)
        throw new Exception("Missed next header");
      this.positionInCurrentReadContainer += amountToRead;

      return this.positionInCurrentReadContainer;
    }

    #endregion

    public override void SetLength(long value)
    {
      this.innerStream.SetLength(value);
    }

    #region Writing

    public override void Write(byte[] buffer, int offset, int count)
    {
      if (count > this.nextWriteHeader)
        throw new IOException("Not enough space in current container");

      //write payload data to inner stream
      this.innerStream.Write(buffer, offset, count);

      //update counters
      this.nextWriteHeader -= count;
      this.positionInCurrentWriteContainer += count;
    }

    public void WriteHeader(ContainerHeader header)
    {
      if (header.ContainerType == ContainerType.CommandBlock)
        System.Threading.Monitor.Enter(commandLock);

      //reserve space for payload
      this.nextWriteHeader = header.PayloadLength;
      this.positionInCurrentWriteContainer = 0;

      //write header to inner stream
      header.Write(this.innerStream);
    }

    public void WriteContainer(Container c)
    {
      this.WriteHeader(c.Header);
      c.WritePayload(this);
      this.innerStream.Flush(); // auto flush when container ends
    }

    #endregion

    protected override void Dispose(bool disposing)
    {
      try
      {
        if (this.innerStream != null)
          this.innerStream.Close();
      }
      finally
      {
        this.innerStream = null;
        base.Dispose(disposing);
      }
    }



  }
}
